In-App Purchase
Introduction
The In-App Purchase feature allows you to monetize your game by offering digital products and content to players. On Android, this is handled through Google Play's billing system, while on iOS, it uses Apple's In-App Purchase system (Store Kit).
Whether you're offering one-time purchases or consumable items, both platforms provide robust APIs to ensure seamless integration, giving your users a familiar and secure purchasing experience.
With Plankton, you can easily integrate in-app purchases across both Android and iOS, streamlining the process for cross-platform monetization.
Before you begin
Prerequisites
- Complete the Plankton setup
Set up your account
Android
- Set up your Google Play developer account
- Enable billing-related features in the Google Play Console
- Create and configure your products in the Google Play Console
To get more info about server-side features of the billing library and details about how to set them up, visit this page.
iOS
- In your App Store Connect, accept the Paid Apps Agreement in the Business section.
- Create in-app purchases and fill the required fields, such as such as product name, description, price, and availability.
- You’ll also need to generate in-app purchase keys and set a tax category, which will allow Apple to calculate the appropriate tax on customer transactions.
Confiure Plankton settings
- Open plankton settings by going to
Edit > Project Settings > Plankton
. - Select the checkbox next to the
In App Purchase
.
Implementation
Now it's time to write some code to make some money out of your game! Always import the Plankton
package in your scripts.
using Plankton;
Initialize
Before using the billing features, you need to initialize the Billing
class first.
You will be notified through the callback argument when the setup process is completed.
Billing.Initialize(succeed => Debug.Log($"{Initialization result:{succeed}"));
Show available products
Now you are ready to query for your available products and display them to your users. Querying for product details is an important step before displaying your products to your users, as it returns localized product information such as price and description.
To query for in-app products, call Billing.GetSkuDetails
. Inside the completion callback, it returns a list of Billing.Detail
which includes the details for each product.
Here’s an example of how to get and display product details:
var productIds = new string[] {"product_sku_1", "product_sku_2"};
Billing.GetSkuDetails((succeed, productDetailList) =>
{
if (succeed)
{
Debug.Log($"Query successful. Retrieved {productDetailList.Count} product details.");
// Loop through the product details and display each one
foreach (var product in productDetailList)
{
Debug.Log($"Product SKU: {product.sku}");
Debug.Log($"Product Title: {product.title}");
Debug.Log($"Product Description: {product.description}");
Debug.Log($"Product Textual Representation of Price: {product.priceFormatted}");
Debug.Log($"Product Currency: {product.priceCurrency}");
Debug.Log($"Product Price (float): {product.priceAmount}");
}
}
else
{
Debug.Log("Failed to retrieve product details.");
}
},
productIds);
On Android, if any of the product IDs in your query are incorrect or don't exist, the entire query will fail, and no product details will be returned.
However on iOS, if one of the SKUs in your query doesn't exist, the method will still return the details for the valid SKUs. Only the missing or incorrect SKU will be excluded from the results.
Make a purchase
To start a purchase request for a specific product, call Billing.StartPurchase
method. Here is the signature of this method:
Billing.StartPurchase(
string sku,
Action<PurchaseStatus, string> callback,
string androidObfuscatedAccountId = "",
string androidObfuscatedProfiledId = ""
)
sku
parameter is the product identifier.callback
is anAction<PurchaseStatus, string>
which is called after the purchase flow has finished. ThePurchaseStatus
determines the result of the purchase(Purchased, Failed, Pending ...). The string value is the purchase token (in Android) or transaction ID (in iOS) if the purchase was successful, otherwise it's an empty string.androidObfuscatedAccountId
andandroidObfuscatedProfiledId
are two optional parameters for Android.
Here's an example of making a purchase:
Billing.StartPurchase("product_sku", (status, tokenOrTransactionId) =>
{
Debug.Log($"Purchase result:{status}, purchase tokenOrTransactionId:{tokenOrTransactionId}");
if (status == Billing.PurchaseStatus.Purchased)
{
// Give the product to the player and finish the purchase.
Billing.FinishPurchase(tokenOrTransactionId, (succeed, tokenOrTransactionId) => {} );
}
else
{
// Handle other purchase statuses...
}
});
Process and finish the purchase
Once a user completes a purchase, your app needs to process and finish the purchase.
Follow these steps to handle a purchase:
- Check the status of purchase to verify it succeeded.
- Deliver the purchased product to the user and finish the process.
In iOS, there’s no distinction between consumable and non-consumable products; all purchases are processed the same way. On Android, however, you need to specify whether a product is consumable or not.
Use the following method for both Android and iOS:
Billing.FinishPurchase(token, (succeed, tokenOrTransactionId) => {
Debug.Log($"Finish purchase result: {succeed}, purchaseTokenOrTransactionId: {tokenOrTransactionId}");
}, isAndroidConsumable);
isAndroidConsumable
: A boolean value to indicate whether the product is consumable (only applicable for Android). On iOS, this parameter is ignored.
Get purchases
To retrieve a list of the user's purchases, use the GetPurchases
method. This ensures that your app processes any unhandled purchases.
Here's an example usage:
Billing.GetPurchases((succeed, purchaseList) =>
{
Debug.Log($"Is successful: {succeed}, Purchased items count: {purchaseList.Count}");
foreach (var item in purchaseList)
{
Debug.Log($"Purchase item --> productId={item.sku}, tokenOrTransactionId={item.token}, purchaseStatus={item.status}");
}
});
- The callback is of the
Action<Boolean, List<Billing.History>>
type. - The boolean value indicates whether the operation was successful or not.
- The
History
object contains details about each purchase, including the purchase token and its status.
Platform Differences
-
On Android,
GetPurchases
returns all ongoing purchases that need to be processed, as well as acknowledged purchases. Consumable purchases that have already been consumed will not appear in the response. -
On iOS,
GetPurchases
only returns ongoing purchases that still need to be processed. It does not include any finished purchases, so this method cannot be used to restore non-consumable purchases. For restoring purchases on iOS, a different mechanism is required.
Restore purchases
Restoring purchases is essential for users who have switched devices or reinstalled your game. It's also possible that your app might not be aware of all the purchases a user has made. Here are some scenarios where your app could lose track or be unaware of purchases:
-
Network Issues during the purchase: The app may not receive a purchase confirmation due to network problems.
-
Multiple devices: A user buys an item on one device and expects to see the item when they switch to another device.
-
Purchases made outside your app: Some purchases, like promotion redemptions, can occur outside the app.
-
Pending transactions (Android only): Transactions that require additional steps between the moment a user initiates a purchase and when the payment method is processed.
The process for restoring purchases differs between Android and iOS.
For Android
To restore purchases on Android, use the GetPurchases
method we discussed earlier.
This method returns a list of the user's purchases that need to be processed. You should check the status of each purchase:
-
If the purchase has an "acked" (acknowledged) status, you should grant the product to the user.
-
If the purchase has a "purchased" status, provide the product to the user and then finish the purchase by acknowledging or consuming it.
Here’s an example:
Billing.GetPurchases((succeed, purchaseList) =>
{
if (succeed)
{
foreach (var purchase in purchaseList)
{
if (purchase.status == "acked")
{
// Provide the product to the user
}
else if (purchase.status == "purchased")
{
// Provide the product to the user, then finish the purchase
var isConsumable = true; // or False
Billing.FinishPurchase(purchase.token, (result, token) => {
Debug.Log($"Finished purchase: {result}, Token: {token}");
}, isConsumable);
}
}
}
});
For iOS
On iOS, restoring purchases is handled using the Billing.RestorePurchases
method.
This method triggers the restoration process, and the result is provided through the onSuccess
callback.
The callback returns a productId(sku) and a transactionID for each restored purchase.
Keep in mind:
-
The
onSuccess
callback may be called once, several times, or not at all, depending on the number of purchases that need to be restored. -
For each restored product, you should provide the product to the user and then finish the purchase to complete the process.
Here’s an example:
Billing.RestorePurchases((productSku, transactionID) =>
{
// Provide the restored product to the user, then finish the purchase
Billing.FinishPurchase(transactionID, (result, transactionID) => {
Debug.Log($"Finished restored purchase: {result}, Transaction ID: {transactionID}");
});
});
Summary
-
Android: Use
Billing.GetPurchases
, check the status of each purchase, and provide the product to the user. For "purchased" status, make sure to finish the purchase after providing the product. -
iOS: Use
Billing.RestorePurchases
and handle each restored purchase in theonSuccess
callback. Provide the product to the user and finish the purchase afterward.
Handle pending transactions (for Android)
Google Play supports pending transactions, or transactions that require one or more additional steps between when a user initiates a purchase and when the payment method for the purchase is processed. Your app should not grant entitlement to these types of purchases until you get notified that the user's payment method was successfully charged.
For example, a user can create a Pending
purchase of an in-app item by choosing cash as their form of payment.
The user can then choose a physical store where they will complete the transaction and receive a code through both notification and email.
When the user arrives at the physical store, they can redeem the code with the cashier and pay with cash. Google then notifies both you and the user that cash has been received.
Your app can then grant entitlement to the user.
In the callback of Purchase
and GetPurchases
methods, detect pending purchases if the Billing.Status
is equal to Status.Pending
.
You should design a flow in your game to check if a pending purchase's status has turned into Status.Purchased
or Status.Failed
.
Then process the purchase again and give entitlement to the user if it was successful.
API Refrences
Defined Types
In this section, we will discuss the enumerated types, variables, and classes associated with this feature:
Enums
Billing.Store
Values | Description | Usage |
---|---|---|
GooglePlay | You have to set GooglePlay as your billing provider. | Billing.Store.GooglePlay |
Billing.PurchaseStatus
Values | Description | Usage |
---|---|---|
Purchased | The item has been purchased. You should give it to the player and call Billing.FinishPurchase method for it. | Billing.PurchaseStatus.Purchased |
Pending | The item is pending for the payment to be done. Later it will either change to Purchased or Failed based on the result of the payment. (Android only) | Billing.PurchaseStatus.Pending |
Failed | The purchase was not successful. | Billing.PurchaseStatus.Failed |
Acked | The item has been purchased and acknowledged. Used for restoration of non-consumable products. (Android only) | Billing.PurchaseStatus.Acked |
Classes
Billing.Detail
Field Name | Field Type | Default Value | Field Description |
---|---|---|---|
sku | string | string.Empty | The product ID |
price | string | string.Empty | Returns formatted price of the item, including its currency sign.For tax-exclusive countries, the price doesn't include tax. |
title | string | string.Empty | Returns the title of the product being sold.The title includes the name of the game which owns the product.Example: 100 Gold Coins (Coin selling app). |
description | string | string.Empty | The description of the product |
Billing.History
Field Name | Field Type | Default Value | Field Description |
---|---|---|---|
sku | string | string.Empty | The product Ids’ |
token | string | string.Empty | A token that uniquely identifies a purchase for a given item and user pair. Later it will be used to consume or acknowledge purchases |
status | string | string.Empty | The status of the purchase |
payload | string | string.Empty | Returns the payload specified when the purchase was acknowledged or consumed.Google has deprecated developer payload, starting with version 2.2 of the Google Play Billing Library. Methods associated with developer payload have been deprecated in version 2.2 and were removed in version 3.0. Note that your game can continue to retrieve developer payload for purchases made using either previous versions of the library or AIDL. |
obfuscated_account_id | string | string.Empty | Specifies an optional obfuscated string that is uniquely associated with the user's account in your game.If you pass this value, Google Play can use it to detect irregular activity, such as many devices making purchases on the same account in a short period of time. Do not use this field to store any Personally Identifiable Information (PII) such as emails in cleartext. Attempting to store PII in this field will result in purchases being blocked. Google Play recommends that you use either encryption or a one-way hash to generate an obfuscated identifier to send to Google Play. |
obfuscated_profile_id | string | string.Empty | Specifies an optional obfuscated string that is uniquely associated with the user's profile in your game.Some games allow users to have multiple profiles within a single account. Use this method to send the user's profile identifier to Google. |
Method Summaries
Method | Arguments | Return Type | Description |
---|---|---|---|
Initialize | System.Action<bool> callback | void | Starts up the billing setup process asynchronously. You will be notified through the callback argument when the setup process is complete. |
StartPurchase | string sku, System.Action<PurchaseStatus, string> callback, string androidObfuscatedAccountId, string androidObfuscatedProfiledId, bool androidAutoAck = false, bool androidAutoConsume = false | void | Use this method to trigger the purchase flow. androidAutoAck and androidAutoConsume are two optional parameters that you can use during the development phase to automatically finish purchase on Android. Learn more about androidObfuscatedAccountId and androidObfuscatedProfiledId in Android |
FinishPurchase | string tokenOrTransactionId, System.Action<bool, string> callback, bool isAndroidConsumable = true | void | Use this method to finish the purchase flow. Developers are required to finish the successful purchases. In Android this method will either consume or acknowledge the purchase based on the value of isAndroidConsumable parameter. In iOS this method will finish the transaction. Failure to finish a purchase will result in that purchase being refunded. |
GetSkuDetails | System.Action<bool, List<Detail>> callback, params string[] skus | void | Returns a list of Billing.Detail which includes the details for each product you passed in the skus parameter. |
GetPurchases | System.Action<bool, List<History>> callback | void | Returns list of the user's unprocessed purchases. In Android it also includes acknowledged purchases. |
RestorePurchases | System.Action<string, string> onSucceed | void | Restores the purchases on iOS and the onSucceed callback will be called once for each restored purchase. |